/*
 * Copyright (c) 2021-2023, WeiStudio
 *
 * License: Apache-2.0
 *
 * Change Logs:
 * Date           Author        Notes
 * 2021-07-13     WeiStudio      the first version
 */



#include "qe_def.h"
#include "qe_log.h"
#include "qe_service.h"
#include "qe_memory.h"
#include "qe_time.h"
#include "qe_buffer.h"
#include "qe_string.h"
#if defined(CONFIG_THREAD)
#include "qe_thread.h"
#endif
#if defined(CONFIG_LOG_ARCHIVE)
#if (CONFIG_ROLLING_FILE != 1)
#error "Logger archive need CONFIG_ROLLING_FILE"
#endif
#if (CONFIG_LIBZLIB != 1)
#error "Logger archive need CONFIG_LIBZLIB"
#endif
#include "qe_rollingfile.h"
#include "zlib.h"
#endif
#include <stdio.h>
#if defined(HAVE_STDARG_H)
#include <stdarg.h>
#else
#error "System don't have stdarg.h"
#endif



#define QELOG_DEFAULT_FLAGS		(QELOG_HMS|QELOG_DM|QELOG_LV|QELOG_CL)
#define QELOG_DEFAULT_LEVEL		CONFIG_LOG_LEVEL_DEFAULT

typedef struct _qelog_domain qelog_domain_t;

struct _qelog_domain
{
	char *name;
	qelog_level_e level;
	qelog_handler_t handler;
	qelog_output_t output;
	qelog_domain_t *next;
};

typedef struct
{
	qelog_level_e level;
	const char *key_short;
	const char *key;
	const char *ansi_color_code;
} qelog_level_entry_t;

typedef struct {
	qe_u32 flags;
	qelog_level_e level;
	qelog_domain_t *domains;
	qelog_output_t output;
	qelog_output_t output_pending;
	qelog_handler_t handler;
    qe_ringbuffer ring;
	qe_uint refcount;
#if defined(CONFIG_LOG_ARCHIVE)
	qe_uint archive_refcount;
	qe_rollingfile *rfile;
#endif
	char *cmp_file;
	qe_size cmp_size;
	qe_bool initialized;
	qe_bool output_en;
} qelog_context_t;

static qelog_level_entry_t level_entrys[] = {
	{QELOG_DEBUG,    "D", "debug",    QELOG_COLOR_LIGHT_GREEN},
	{QELOG_INFO,     "I", "info",	  QELOG_COLOR_LIGHT_CYAN},
	{QELOG_NOTICE,   "N", "notice",	  QELOG_COLOR_LIGHT_PINK},
	{QELOG_WARNING,  "W", "warning",  QELOG_COLOR_LIGHT_YELLOW},
	{QELOG_CRITICAL, "C", "critical", QELOG_COLOR_LIGHT_BLUE},
	{QELOG_ERROR,    "E", "error",	  QELOG_COLOR_LIGHT_RED},
	{QELOG_FATAL,    "F", "fatal",	  QELOG_COLOR_RED},
};

#if defined(CONFIG_LOG_DEFAULT_HANDLER)
static void qe_log_default_handler(const char *domain_name, qelog_level_e level, 
	const char *file, const char *func, int line, const char *message);
#endif

#if defined(CONFIG_LOG_DEFAULT_OUTPUT)
static void qe_log_default_output(const char *message);
#endif

static qelog_domain_t *find_domain(const char *name);



static qelog_context_t log = {
	.flags       = QELOG_DEFAULT_FLAGS,
	.level       = QELOG_DEFAULT_LEVEL,
	.domains     = QE_NULL,
#if defined(CONFIG_LOG_DEFAULT_HANDLER)
	.handler     = qe_log_default_handler,
#else
	.handler     = QE_NULL,
#endif
#if defined(CONFIG_LOG_DEFAULT_OUTPUT)
	.output      = qe_log_default_output,
#else
	.output      = QE_NULL,
#endif
	.output_pending = QE_NULL,
    .ring.buf    = QE_NULL,
#if defined(CONFIG_LOG_ARCHIVE)
	.archive_refcount = 0,
	.rfile       = QE_NULL,
#endif
    .initialized = qe_false,
	.output_en   = qe_true,
	.cmp_file    = QE_NULL,
	.cmp_size    = 0,
	.refcount    = 0,
};

#if defined(CONFIG_LOG_ARCHIVE)
static qe_size get_file_size(const char *name)
{
	qe_size size;
	FILE *fp = fopen(name, "r");
	if (!fp)
		return 0;
	fseek(fp, 0, SEEK_END);
	size = ftell(fp);
	fseek(fp, 0, SEEK_SET);
	fclose(fp);
	return size;
}

static void log_compress(const char *name)
{
	int len;
	qe_size size;
	char buf[16384];
	FILE *fp = fopen(name, "rb");
	if (!fp)
		return;
	size = get_file_size(log.cmp_file);
	gzFile out = gzopen(log.cmp_file, "a");
	if (!out) {
		fclose(fp);
		return;
	}

	if (size > log.cmp_size) {
		fclose(fp);
		gzclose(out);
		return;
	}
	
    while(1) {
        len = fread(buf, 1, 16384, fp);
        if (len == 0)
            break;
        gzwrite(out, buf, len);
    }

	fclose(fp);
	gzclose(out);
}

static void archive_record(const char *message, qe_uint len)
{
	if (!log.rfile)
		return;
	qe_rollingfile_append_buffer(log.rfile, message, len);
	qe_rollingfile_append_string(log.rfile, "\n");
}

qe_ret qelog_set_archive(char *dir, char *name, char *exntension, 
	int num_roll, qe_size max_size, qe_size cmp_size)
{
	if (!(log.flags & QELOG_AR) || !log.initialized || log.rfile) {
		return qe_err_param;
	}

	qe_rollingfile *rfile = qe_rollingfile_new(dir, name, exntension, 
		num_roll, max_size);
	if (!rfile) {
		return qe_err_mem;
	}

	log.rfile = rfile;

	log.cmp_file = qe_strdup_format("%s/%s-archive.%s.zip", 
		dir, name, exntension);
	log.cmp_size = cmp_size;

	qe_rollingfile_set_hook(rfile, QE_ROLLINGFILE_HOOK_BEFORE_REMOVE, 
		log_compress);

	return qe_ok;
}
#endif

#if defined(CONFIG_LOG_DEFAULT_HANDLER)

#if defined(CONFIG_NANO)
static qe_time_t get_pts(void)
{
	return qe_time_us();
}
#else
static double get_pts(void)
{
	qe_time_t us = qe_time_us();
	return us / 1000000.0;
}
#endif

const char *qelog_get_level_str(qelog_level_e level)
{
	if (level > QELOG_FATAL) {
		return QE_NULL;
	}

	return level_entrys[level].key_short;
}

static void qe_log_default_handler(const char *domain_name, qelog_level_e level, 
	const char *file, const char *func, int line, const char *message)
{
	qelog_domain_t *domain;
	char buffer[CONFIG_LOG_BUF_SIZE];
	qelog_level_entry_t *ent = &level_entrys[level];
	const char *color = level_entrys[level].ansi_color_code;
	
	qe_memset(buffer, 0x0, CONFIG_LOG_BUF_SIZE);
	qe_strb strb = qe_strb_init(buffer, CONFIG_LOG_BUF_SIZE);

	/* color start */
	if (log.flags & QELOG_CL)
		qe_strb_format(strb, "%s", color);

	/* time format */
	if (log.flags & (QELOG_PTS | QELOG_DATE | QELOG_HMS)) {
		if (log.flags & QELOG_PTS) {
#if defined(CONFIG_NANO)
			qe_strb_format(strb, "[%llu] ", get_pts());
#else	
			qe_strb_format(strb, "[%.6f] ", get_pts());
#endif
		} else {
			qe_date_t date;
			qe_date(&date);
			if (log.flags & QELOG_DATE) {
				qe_strb_format(strb, "[%d-%02d-%02d %02i:%02i:%02i] ", 
					date.year, date.mon, date.day, date.hour, date.min, date.sec);
			} else {
				qe_strb_format(strb, "[%02i:%02i:%02i] ", 
					date.hour, date.min, date.sec);
			}
		}
	}

	/* level */
	if (log.flags & QELOG_LV)
		qe_strb_format(strb, "%s ", ent->key_short);

	/* domain name */
	if (log.flags & QELOG_DM) {
		if (log.flags & QELOG_DNL) {
			qe_strb_format(strb, "%-"QELOG_DOMAIN_NAME_STR(CONFIG_LOG_DOMAIN_NAME_MAX)"s ",
				domain_name);
		} else {
			qe_strb_format(strb, "%s ", domain_name);
		}
	}

#if defined(CONFIG_THREAD)
	/* pid */
	if (log.flags & QELOG_PID) {
		int pid = qe_get_pid();
		qe_strb_format(strb, "<%d> ", pid);
	}
#endif

	if (log.flags & (QELOG_PTS|QELOG_HMS|QELOG_DATE|QELOG_DM|QELOG_LV|QELOG_PID))
		qe_strb_format(strb, ": ");

	/* filename */
	if (log.flags & QELOG_FILE) {
		qe_strb_format(strb, "%s ", file);
	}

	/* func */
	if (log.flags & QELOG_FUNC) {
		qe_strb_format(strb, "%s ", func);
	}

	/* line */
	if (log.flags & QELOG_LINE) {
		qe_strb_format(strb, "%d ", line);
	}

	/* message */
	qe_strb_format(strb, "%s", message);

	/* color end */
	if (log.flags & QELOG_CL) {
		qe_strb_format(strb, "%s", QELOG_COLOR_RESET);
	}

	qe_strb_format(strb, "\r\n");

	/* write to ringbuffer */
	if ((log.flags & QELOG_RB) && (log.ring.buf)) {
		qe_ringbuffer_write(&log.ring, strb.head, strb.len);
	}

#if defined(CONFIG_LOG_ARCHIVE)
	/*
	 * Logging archive using rollingfile, there is logging in rollingfile,
	 * so there is a recursive of logging, we use a reference count to 
	 * avoid archive recursive
	 */
	if (log.archive_refcount == 0) {
		log.archive_refcount++;
		/*
		 * The color uses ANSI encoding, and archived log is not applicable, 
		 * so it is necessary to filter the ANSI encoding
		 */
		if (log.flags & QELOG_AR) {
			if (log.flags & QELOG_CL) {
				char *ar_start = strb.head + qe_strlen(color);
				int ar_size = strb.len - qe_strlen(color) - qe_strlen(QELOG_COLOR_RESET) - qe_strlen("\n");
				archive_record(ar_start, ar_size);
			} else {
				archive_record(strb.head, strb.len);
			}
		}
		log.archive_refcount--;
	}
	
#endif

	if (log.output_en) {
		domain = find_domain(domain_name);
		if (!domain || !domain->output) {
			return log.output(strb.head);
		}
		return domain->output(strb.head);
	}
}
#endif

#if defined(CONFIG_LOG_DEFAULT_OUTPUT)
static void qe_log_default_output(const char *message)
{
	qe_printf("%s", message);
}
#endif

static qelog_domain_t *find_domain(const char *name)
{
	register qelog_domain_t *domain;

	domain = log.domains;
	while (domain) {
		if (qe_strcmp(domain->name, name) == 0)
			return domain;
		domain = domain->next;
	}
	return QE_NULL;
}

static qelog_domain_t *new_domain(const char *name)
{
	register qelog_domain_t *domain;

	domain = qe_malloc(sizeof(qelog_domain_t));
	domain->name     = qe_strdup(name);
	domain->level    = QELOG_DEFAULT_LEVEL;
	domain->handler  = log.handler;
	domain->output   = log.output;
	domain->next     = log.domains;
	log.domains      = domain;
	return domain;
}

static qelog_handler_t domain_get_handler(qelog_domain_t *domain)
{
	if (domain && domain->handler) {
		return domain->handler;
	}

	return log.handler;
}

void qelog_set_handler(const char *name, qelog_level_e level, 
	qelog_handler_t handler)
{
	qelog_domain_t *domain;

	if (!name)
		name = "";

	domain = find_domain(name);
	if (!domain)
		domain = new_domain(name);

	domain->level = level;
	domain->handler = handler;
}

void qelog_set_default_handler(qelog_handler_t handler)
{
	log.handler = handler;
}

void qelog_set_default_output(qelog_output_t output)
{
	if (log.refcount == 0)
		log.output = output;
	else
		log.output_pending = output;
}

void qelog_domain_set_level(qe_const_str name, qelog_level_e level)
{
	qelog_domain_t *domain;

	if (!name)
		name = "";

	domain = find_domain(name);
	if (!domain)
		domain = new_domain(name);

	domain->level = level;
}

void qelog_domain_set_handler(qe_const_str name, qelog_handler_t handler)
{
	qelog_domain_t *domain;

	if (!name)
		name = "";

	domain = find_domain(name);
	if (!domain)
		domain = new_domain(name);

	domain->handler = handler;
}

void qelog_domain_set_output(qe_const_str name, qelog_output_t output)
{
	qelog_domain_t *domain;

	if (!name)
		name = "";

	domain = find_domain(name);
	if (!domain)
		domain = new_domain(name);

	domain->output = output;
}

void qelog_domain_show(void)
{
	qe_str handle_str;
	qe_str output_str;
	qe_const_str head_fmt = "%-16s%-10s%-10s%-10s"QE_ENDLINE;
	qe_const_str data_fmt = "%-16s%-10s%-10s%-10s"QE_ENDLINE;
	qelog_domain_t *domain;

	qe_printf("-----------------------------------------------"QE_ENDLINE);
	qe_printf(head_fmt, "name", "level", "handler", "output");
	qe_printf("-----------------------------------------------"QE_ENDLINE);
	domain = log.domains;
	while (domain) {

		if (domain->handler == QE_NULL) {
			handle_str = "none";
		} else if (domain->handler == log.handler) {
			handle_str = "default";
		} else {
			handle_str = "custom";
		}

		if (domain->output == QE_NULL) {
			output_str = "none";
		} else if (domain->output == log.output) {
			output_str = "default";
		} else {
			output_str = "custom";
		}

		qe_printf(data_fmt,
			domain->name,
			level_entrys[domain->level].key,
			handle_str,
			output_str);

		domain = domain->next;
	}
	qe_printf("-----------------------------------------------"QE_ENDLINE);
}

void qelog_set_output_enable(qe_bool en)
{
	log.output_en = en;
}

void qelog_show_summary(void)
{
	int i;

	qe_printf("qelog"QE_ENDLINE);

	qe_printf("settings-->"QE_ENDLINE);
	qe_printf("  level      : %d"QE_ENDLINE, log.level);
	qe_printf("  flags      : 0x%x"QE_ENDLINE, log.flags);
	qe_printf("  output     : %s"QE_ENDLINE, log.output_en ? "enable" : "disable");
	if (log.flags & QELOG_RB) {
	qe_printf("  ringbuffer : %p %d %d"QE_ENDLINE,
		log.ring.buf, log.ring.size, qe_ringbuffer_wait(&log.ring));
	}

	qe_printf("levels-->"QE_ENDLINE);
	for (i=0; i<QELOG_DISABLE; i++) {
		qe_printf("  %d : %s %s"QE_ENDLINE, i, 
			level_entrys[i].key_short,
			level_entrys[i].key);
	}
}

static qe_bool level_visible(qelog_level_e level)
{
	return (log.level <= level);
}

static void internal_domain_setup(void)
{
	qelog_domain_set_level(QELOG_DOMAIN_INITCALL, QELOG_DEFAULT_LEVEL);
	qelog_domain_set_level(QELOG_DOMAIN_BIT, QELOG_DEFAULT_LEVEL);
	qelog_domain_set_level(QELOG_DOMAIN_HEX, QELOG_DEFAULT_LEVEL);
	qelog_domain_set_level(QELOG_DOMAIN_HASH, QELOG_DEFAULT_LEVEL);
	qelog_domain_set_level(QELOG_DOMAIN_PACK, QELOG_DEFAULT_LEVEL);
	qelog_domain_set_level(QELOG_DOMAIN_DBGCTRL, QELOG_DEFAULT_LEVEL);
	qelog_domain_set_level(QELOG_DOMAIN_BUFFER, QELOG_DEFAULT_LEVEL);
	qelog_domain_set_level(QELOG_DOMAIN_ARGPARSE, QELOG_DEFAULT_LEVEL);
	qelog_domain_set_level(QELOG_DOMAIN_GRAPH, QELOG_DEFAULT_LEVEL);

#if (CONFIG_ENABLE_SHELL == 1)
	qelog_domain_set_level(QELOG_DOMAIN_SHELL, QELOG_DEFAULT_LEVEL);
#endif

#if (CONFIG_ENABLE_SDTRACE == 1)
	qelog_domain_set_level(QELOG_DOMAIN_SDTRACE, QELOG_DEFAULT_LEVEL);
#endif

#if (CONFIG_DM == 1)
	qelog_domain_set_level(QELOG_DOMAIN_DM, QELOG_DEFAULT_LEVEL);
#if (CONFIG_IIC == 1)
	qelog_domain_set_level(QELOG_DOMAIN_IIC, QELOG_DEFAULT_LEVEL);
#endif
#if (CONFIG_SERIAL == 1)
	qelog_domain_set_level(QELOG_DOMAIN_SERIAL, QELOG_DEFAULT_LEVEL);
#endif
#if (CONFIG_SPI == 1)
	qelog_domain_set_level(QELOG_DOMAIN_SPI, QELOG_DEFAULT_LEVEL);
#endif
#if (CONFIG_SPI_FLASH == 1)
	qelog_domain_set_level(QELOG_DOMAIN_SPIF, QELOG_DEFAULT_LEVEL);
#endif

#endif

#if (CONFIG_ROLLING_FILE)
	qelog_domain_set_level(QELOG_DOMAIN_ROLLFILE, QELOG_DEFAULT_LEVEL);
#endif

#if (CONFIG_EARLYCON)
	qelog_domain_set_level(QELOG_DOMAIN_EARLYCON, QELOG_DEFAULT_LEVEL);
#endif
}

qe_ret qelog_init(qelog_level_e level, qe_u32 flags)
{
    if (log.initialized) {
		return qe_err_param;
	}

	if (level >= QELOG_SIZEOF || level < QELOG_DEBUG) {
		return qe_err_param;
	}

	internal_domain_setup();
    
    log.flags = flags;
    log.level = level;
    log.initialized = qe_true;
	return qe_ok;
}

qe_ret qelog_set_level(qelog_level_e level)
{
	if (level >= QELOG_SIZEOF || level < QELOG_DEBUG) {
		return qe_err_param;
	}
	
	log.level = level;
	return qe_ok;
}

qelog_level_e qelog_get_level(void)
{
	return log.level;
}

void qelog_set_flags(qe_u32 flags)
{
	log.flags = flags;
}

qe_u32 qelog_get_flags(void)
{
	return log.flags;
}

qe_ringbuffer *qelog_get_buffer(void)
{
    return &log.ring;
}

qe_ret qelog_set_buffer(void *buf, qe_u32 size) 
{
	if (!(log.flags & QELOG_RB) || !log.initialized || log.ring.buf)
		return qe_err_param;

	if (!buf) {
		char *alloc = qe_malloc(size);
		if (!alloc) {
			return qe_err_mem;
		}
		qe_ringbuffer_init(&log.ring, alloc, size);
	} else {
		qe_ringbuffer_init(&log.ring, buf, size);
	}
	
	return qe_ok;
}

static char *strdup_vprintf(const char *format,
    va_list args)
{
 	char *string = QE_NULL;
  	qe_vasprintf(&string, format, args);
  	return string;
}

static inline const char *format_string(const char *format, 
	va_list args, char **out_allocated_string)
{
	/* If there is no formatting to be done, avoid an allocation */
	if (qe_strchr (format, '%') == QE_NULL) {
		*out_allocated_string = QE_NULL;
		return format;
	} else {
		*out_allocated_string = strdup_vprintf(format, args);
		return *out_allocated_string;
	}
}

void qe_logv(const char *name, qelog_level_e level,
    const char *file, const char *func, int line, const char *fmt, va_list args)
{
	qelog_domain_t *domain;
	qelog_handler_t handler;
	char buffer[CONFIG_LOG_BUF_SIZE+1], *msg_alloc = QE_NULL;
	const char *msg;

	if (log.initialized == qe_false)
		return;

	log.refcount++;

	if (log.flags & QELOG_RECUR) {
		qe_vsnprintf(buffer, CONFIG_LOG_BUF_SIZE, fmt, args);
		msg = buffer;
	} else {
		msg = format_string(fmt, args, &msg_alloc);
	}

	domain = find_domain(name ? name : "");
	if (domain && domain->level > level) {
		goto __exit;
	} else if (!domain) {
		/* low level log don't handle */
		if (level < log.level || level >= QELOG_DISABLE) {
			goto __exit;
		}
	}
	
	handler = domain_get_handler(domain);
	if (handler) {
		handler(name, level, file, func, line, msg);
	}

__exit:
	if (msg_alloc)
		qe_free(msg_alloc);
	log.refcount--;
}

void qelog(const char *name, qelog_level_e level,
    const char *file, const char *func, int line, const char *fmt, ...)
{
	va_list args;
	va_start(args, fmt);
	qe_logv(name, level, file, func, line, fmt, args);
	va_end(args);
}

void qelog_hexdump(const char *domain_name, qe_u8 level, const char *file,
	const char *func, int linenr, const void *vbuf, unsigned int len)
{
	unsigned int n;
	unsigned char *buf = (unsigned char *)vbuf;
	qelog_domain_t *domain;

	if (!len || !vbuf)
		return;

	domain = find_domain(domain_name ? domain_name : "");
	if (domain && domain->level > level) {
		return;
	} else if (!domain) {
		/* low level log don't handle */
		if (level < log.level || level >= QELOG_DISABLE) {
			return;
		}
		domain = new_domain(domain_name);
		domain->level = log.level;
	}

	for (n = 0; n < len;) {
		unsigned int start = n, m;
		char line[128], *p = line;

		p += qe_snprintf(p, 10, "%04X: ", start);
		for (m = 0; m < 16 && n < len; m++)
			p += qe_snprintf(p, 5, "%02X ", buf[n++]);

		while (m++ < 16)
			p += qe_snprintf(p, 5, "   ");

		p += qe_snprintf(p, 6, "   ");
		for (m = 0; m < 16 && (start + m) < len; m++) {
			if (buf[start + m] >= ' ' && buf[start + m] < 127)
				*p++ = buf[start + m];
			else
				*p++ = '.';
		}

		while (m++ < 16)
			*p++ = ' ';

		*p = '\0';
		qelog(domain_name, level, file, func, linenr, "%s", line);
	}
}

void qelog_bitdump(const char *domain_name, qe_u8 level, const char *file, 
	const char *func, int linenr, const void *vbuf, qe_size len)
{
	unsigned int n;
	qe_u32 *buf = (qe_u32 *)vbuf;
	qelog_domain_t *domain;

	if (!len || !vbuf)
		return;

	domain = find_domain(domain_name ? domain_name : "");
	if (domain && domain->level > level) {
		return;
	} else if (!domain) {
		/* low level log don't handle */
		if (level < log.level || level >= QELOG_DISABLE) {
			return;
		}
		domain = new_domain(domain_name);
		domain->level = log.level;
	}

    for (n=0; n<len; n+=32) {
		unsigned int start = n, m;
		char line[80], *p = line;

		p += qe_snprintf(p, 10, "%04d:  ", start);
		for (m = 0; m < 32 && n < len; m++)
			p += qe_snprintf(p, 5, "%d ", (buf[n/32]&(1<<m))>0);
		while (m++ < 32)
			p += qe_snprintf(p, 5, "   ");
		
		p += qe_snprintf(p, 5, "   ");
		*p = '\0';
		qelog(domain_name, level, file, func, linenr, "%s", line);
		(void)line;
	}
}